Kuasai pustaka unittest.mock Python. Panduan mendalam tentang test double, objek mock, stub, fake, dan dekorator patch untuk pengujian unit yang kuat dan terisolasi.
Objek Mock Python: Panduan Komprehensif Implementasi Test Double
Dalam dunia pengembangan perangkat lunak modern, menulis kode hanyalah separuh dari perjuangan. Memastikan bahwa kode tersebut andal, kuat, dan berfungsi seperti yang diharapkan adalah separuh lainnya yang sama pentingnya. Di sinilah pengujian otomatis berperan. Pengujian unit, khususnya, adalah praktik mendasar yang melibatkan pengujian komponen atau 'unit' individual dari sebuah aplikasi secara terisolasi. Namun, isolasi ini seringkali lebih mudah diucapkan daripada dilakukan. Aplikasi dunia nyata adalah jalinan kompleks dari objek, layanan, dan sistem eksternal yang saling terhubung. Bagaimana Anda bisa menguji satu fungsi jika ia bergantung pada database, API pihak ketiga, atau bagian kompleks lain dari sistem Anda?
Jawabannya terletak pada teknik yang kuat: penggunaan Test Doubles. Dan dalam ekosistem Python, alat utama untuk membuatnya adalah pustaka unittest.mock yang serbaguna dan sangat diperlukan. Panduan ini akan membawa Anda menyelami dunia mock dan test double di Python. Kita akan menjelajahi 'mengapa' di baliknya, menjelaskan berbagai jenisnya, dan memberikan contoh praktis dunia nyata menggunakan unittest.mock untuk membantu Anda menulis pengujian yang lebih bersih, lebih cepat, dan lebih efektif.
Apa Itu Test Double dan Mengapa Kita Membutuhkannya?
Bayangkan Anda sedang membangun sebuah fungsi yang mengambil profil pengguna dari database perusahaan Anda dan kemudian memformatnya. Tanda tangan fungsi tersebut mungkin terlihat seperti ini: get_formatted_user_profile(user_id, db_connection).
Untuk menguji fungsi ini, Anda menghadapi beberapa tantangan:
- Ketergantungan pada Sistem Aktif: Pengujian Anda akan membutuhkan database yang berjalan. Ini membuat pengujian menjadi lambat, rumit untuk disiapkan, dan bergantung pada keadaan serta ketersediaan sistem eksternal.
- Ketidakpastian: Data di dalam database mungkin berubah, menyebabkan pengujian Anda gagal meskipun logika pemformatan Anda benar. Ini membuat pengujian menjadi 'flaky' atau tidak deterministik.
- Kesulitan dalam Menguji Kasus Tepi (Edge Cases): Bagaimana Anda akan menguji apa yang terjadi jika koneksi database gagal, atau jika ia mengembalikan pengguna yang kehilangan beberapa data? Mensimulasikan skenario spesifik ini dengan database nyata bisa sangat sulit.
Test Double adalah istilah generik untuk objek apa pun yang menggantikan objek nyata selama pengujian. Dengan mengganti db_connection yang asli dengan test double, kita dapat memutuskan ketergantungan pada database yang sebenarnya dan mengambil kendali penuh atas lingkungan pengujian.
Menggunakan test double memberikan beberapa manfaat utama:
- Isolasi: Mereka memungkinkan Anda untuk menguji unit kode Anda (misalnya, logika pemformatan) secara terisolasi penuh dari dependensinya (misalnya, database). Jika pengujian gagal, Anda tahu masalahnya ada di unit yang diuji, bukan di tempat lain.
- Kecepatan: Mengganti operasi lambat seperti permintaan jaringan atau kueri database dengan test double dalam memori membuat rangkaian pengujian Anda berjalan secara dramatis lebih cepat. Pengujian yang cepat lebih sering dijalankan, menghasilkan siklus umpan balik yang lebih ketat bagi pengembang.
- Determinisme: Anda dapat mengonfigurasi test double untuk mengembalikan data yang dapat diprediksi setiap kali pengujian dijalankan. Ini menghilangkan pengujian yang 'flaky' dan memastikan bahwa pengujian yang gagal menunjukkan masalah yang sebenarnya.
- Kemampuan untuk Menguji Kasus Tepi: Anda dapat dengan mudah mengonfigurasi double untuk mensimulasikan kondisi kesalahan, seperti memunculkan
ConnectionErroratau mengembalikan data kosong, memungkinkan Anda untuk memverifikasi bahwa kode Anda menangani situasi ini dengan baik.
Taksonomi Test Double: Lebih dari Sekadar "Mock"
Meskipun pengembang sering menggunakan istilah "mock" secara generik untuk merujuk pada test double apa pun, akan sangat membantu untuk memahami terminologi yang lebih tepat yang diciptakan oleh Gerard Meszaros dalam bukunya "xUnit Test Patterns." Mengetahui perbedaan ini membantu Anda berpikir lebih jernih tentang apa yang ingin Anda capai dalam pengujian Anda.
1. Dummy
Objek Dummy adalah test double yang paling sederhana. Ia dilewatkan untuk mengisi daftar parameter tetapi tidak pernah benar-benar digunakan. Metodenya biasanya tidak dipanggil. Anda menggunakan dummy ketika Anda perlu memberikan argumen ke sebuah metode, tetapi Anda tidak peduli dengan perilaku argumen tersebut dalam konteks pengujian spesifik.
Contoh: Jika sebuah fungsi memerlukan objek 'logger' tetapi pengujian Anda tidak peduli dengan apa yang dicatat, Anda bisa memberikan objek dummy.
2. Fake
Objek Fake memiliki implementasi yang berfungsi, tetapi merupakan versi yang jauh lebih sederhana dari objek produksi. Ia tidak menggunakan sumber daya eksternal dan menggantikan implementasi yang berat dengan yang ringan. Contoh klasiknya adalah database dalam memori yang menggantikan koneksi database nyata. Ia benar-benar berfungsi—Anda dapat menambahkan data ke dalamnya dan membaca data darinya—tetapi di baliknya hanyalah sebuah dictionary atau list sederhana.
3. Stub
Stub menyediakan jawaban yang telah diprogram sebelumnya atau "kalengan" untuk panggilan metode yang dibuat selama pengujian. Ini digunakan ketika Anda membutuhkan kode Anda untuk menerima data spesifik dari sebuah dependensi. Misalnya, Anda bisa membuat stub untuk metode seperti api_client.get_user(user_id=123) agar selalu mengembalikan dictionary pengguna tertentu, tanpa benar-benar melakukan panggilan API.
4. Spy
Spy adalah stub yang juga merekam beberapa informasi tentang bagaimana ia dipanggil. Misalnya, ia mungkin merekam berapa kali sebuah metode dipanggil atau argumen apa yang dilewatkan kepadanya. Ini memungkinkan Anda untuk "memata-matai" interaksi antara kode Anda dan dependensinya dan kemudian membuat asersi tentang interaksi tersebut setelahnya.
5. Mock
Mock adalah jenis test double yang paling 'sadar'. Ia adalah objek yang telah diprogram sebelumnya dengan ekspektasi tentang metode mana yang akan dipanggil, dengan argumen apa, dan dalam urutan apa. Pengujian yang menggunakan objek mock biasanya akan gagal tidak hanya jika kode yang diuji menghasilkan hasil yang salah tetapi juga jika ia tidak berinteraksi dengan mock dengan cara yang tepat seperti yang diharapkan. Mock sangat bagus untuk verifikasi perilaku—memastikan urutan tindakan tertentu terjadi.
Pustaka unittest.mock Python menyediakan satu kelas yang kuat yang dapat bertindak sebagai Stub, Spy, atau Mock, tergantung pada bagaimana Anda menggunakannya.
Memperkenalkan Pustaka Andal Python: `unittest.mock`
Sebagai bagian dari pustaka standar Python sejak versi 3.3, unittest.mock adalah solusi kanonis untuk membuat test double. Fleksibilitas dan kekuatannya menjadikannya alat penting bagi setiap pengembang Python yang serius. Jika Anda menggunakan versi Python yang lebih lama, Anda dapat menginstal pustaka yang di-backport melalui pip: pip install mock.
Inti dari pustaka ini berkisar pada dua kelas utama: Mock dan saudaranya yang lebih mumpuni, MagicMock. Objek-objek ini dirancang untuk menjadi sangat fleksibel, membuat atribut dan metode secara dinamis saat Anda mengaksesnya.
Pembahasan Mendalam: Kelas `Mock` dan `MagicMock`
Objek `Mock`
Objek `Mock` adalah bunglon. Anda dapat membuatnya, dan ia akan segera merespons setiap akses atribut atau panggilan metode, mengembalikan objek Mock lain secara default. Ini memungkinkan Anda untuk merangkai panggilan dengan mudah selama penyiapan.
# Di dalam file tes...
from unittest.mock import Mock
# Membuat objek mock
mock_api = Mock()
# Mengakses atribut akan membuatnya dan mengembalikan mock lain
print(mock_api.users)
# Output: <Mock name='mock.users' id='...'>
# Memanggil metode juga mengembalikan mock secara default
print(mock_api.users.get(id=1))
# Output: <Mock name='mock.users.get()' id='...'>
Perilaku default ini tidak terlalu berguna untuk pengujian. Kekuatan sebenarnya datang dari mengonfigurasi mock agar berperilaku seperti objek yang digantikannya.
Mengonfigurasi Nilai Kembalian dan Efek Samping
Anda dapat memberitahu metode mock apa yang harus dikembalikan menggunakan atribut return_value. Inilah cara Anda membuat sebuah Stub.
from unittest.mock import Mock
# Membuat mock untuk layanan data
mock_service = Mock()
# Mengonfigurasi nilai kembalian untuk panggilan metode
mock_service.get_data.return_value = {'id': 1, 'name': 'Test Data'}
# Sekarang ketika kita memanggilnya, kita mendapatkan nilai yang telah dikonfigurasi
result = mock_service.get_data()
print(result)
# Output: {'id': 1, 'name': 'Test Data'}
Untuk mensimulasikan kesalahan, Anda dapat menggunakan atribut side_effect. Ini sempurna untuk menguji penanganan kesalahan kode Anda.
from unittest.mock import Mock
mock_service = Mock()
# Mengonfigurasi metode untuk memunculkan pengecualian
mock_service.get_data.side_effect = ConnectionError("Failed to connect to service")
# Memanggil metode sekarang akan memunculkan pengecualian
try:
mock_service.get_data()
except ConnectionError as e:
print(e)
# Output: Failed to connect to service
Metode Asersi untuk Verifikasi
Objek mock juga bertindak sebagai Spy dan Mock dengan merekam bagaimana mereka digunakan. Anda kemudian dapat menggunakan serangkaian metode asersi bawaan untuk memverifikasi interaksi ini.
mock_object.method.assert_called(): Memastikan metode dipanggil setidaknya sekali.mock_object.method.assert_called_once(): Memastikan metode dipanggil tepat satu kali.mock_object.method.assert_called_with(*args, **kwargs): Memastikan metode terakhir kali dipanggil dengan argumen yang ditentukan.mock_object.method.assert_any_call(*args, **kwargs): Memastikan metode dipanggil dengan argumen ini kapan saja.mock_object.method.assert_not_called(): Memastikan metode tidak pernah dipanggil.mock_object.call_count: Properti integer yang memberi tahu Anda berapa kali metode dipanggil.
from unittest.mock import Mock
mock_notifier = Mock()
# Bayangkan ini adalah fungsi yang sedang kita uji
def process_and_notify(data, notifier):
if data.get('critical'):
notifier.send_alert(message="Critical event occurred!")
# Kasus uji 1: Data kritis
process_and_notify({'critical': True}, mock_notifier)
mock_notifier.send_alert.assert_called_once_with(message="Critical event occurred!")
# Atur ulang mock untuk tes berikutnya
mock_notifier.reset_mock()
# Kasus uji 2: Data tidak kritis
process_and_notify({'critical': False}, mock_notifier)
mock_notifier.send_alert.assert_not_called()
Objek `MagicMock`
`MagicMock` adalah subkelas dari `Mock` dengan perbedaan utama: ia memiliki implementasi default untuk sebagian besar metode "magic" atau "dunder" Python (misalnya, __len__, __str__, __iter__). Jika Anda mencoba menggunakan `Mock` biasa dalam konteks yang memerlukan salah satu metode ini, Anda akan mendapatkan error.
from unittest.mock import Mock, MagicMock
# Menggunakan Mock biasa
mock_list = Mock()
try:
len(mock_list)
except TypeError as e:
print(e) # Output: 'Mock' object has no len()
# Menggunakan MagicMock
magic_mock_list = MagicMock()
print(len(magic_mock_list)) # Output: 0 (secara default)
# Kita juga bisa mengonfigurasi nilai kembalian metode magic
magic_mock_list.__len__.return_value = 100
print(len(magic_mock_list)) # Output: 100
Aturan praktis: Mulailah dengan `MagicMock`. Umumnya lebih aman dan mencakup lebih banyak kasus penggunaan, seperti me-mock objek yang digunakan dalam loop for (membutuhkan __iter__) atau pernyataan with (membutuhkan __enter__ dan __exit__).
Implementasi Praktis: Dekorator `patch` dan Context Manager
Membuat mock adalah satu hal, tetapi bagaimana cara agar kode Anda menggunakannya alih-alih objek asli? Di sinilah `patch` berperan. `patch` adalah alat yang kuat di `unittest.mock` yang untuk sementara menggantikan objek target dengan mock selama durasi pengujian.
`@patch` sebagai Dekorator
Cara paling umum untuk menggunakan `patch` adalah sebagai dekorator pada metode pengujian Anda. Anda memberikan path string ke objek yang ingin Anda ganti.
Katakanlah kita memiliki fungsi yang mengambil data dari API web menggunakan pustaka `requests` yang populer:
# di file: my_app/data_fetcher.py
import requests
def get_user_data(user_id):
response = requests.get(f"https://api.example.com/users/{user_id}")
response.raise_for_status() # Memunculkan pengecualian untuk kode status yang buruk
return response.json()
Kita ingin menguji fungsi ini tanpa melakukan panggilan jaringan nyata. Kita bisa me-patch `requests.get`:
# di file: tests/test_data_fetcher.py
import unittest
from unittest.mock import patch, Mock
from my_app.data_fetcher import get_user_data
class TestDataFetcher(unittest.TestCase):
@patch('my_app.data_fetcher.requests.get')
def test_get_user_data_success(self, mock_get):
"""Menguji pengambilan data yang berhasil."""
# Mengonfigurasi mock untuk mensimulasikan respons API yang berhasil
mock_response = Mock()
mock_response.json.return_value = {'id': 1, 'name': 'John Doe'}
mock_response.raise_for_status.return_value = None # Tidak melakukan apa-apa jika berhasil
mock_get.return_value = mock_response
# Panggil fungsi kita
user_data = get_user_data(1)
# Memastikan fungsi kita melakukan panggilan API yang benar
mock_get.assert_called_once_with('https://api.example.com/users/1')
# Memastikan fungsi kita mengembalikan data yang diharapkan
self.assertEqual(user_data, {'id': 1, 'name': 'John Doe'})
Perhatikan bagaimana `patch` membuat `MagicMock` dan meneruskannya ke metode pengujian kita sebagai argumen `mock_get`. Di dalam pengujian, setiap panggilan ke `requests.get` di dalam `my_app.data_fetcher` dialihkan ke objek mock kita.
`patch` sebagai Context Manager
Terkadang Anda hanya perlu me-patch sesuatu untuk sebagian kecil dari pengujian. Menggunakan `patch` sebagai context manager dengan pernyataan `with` sangat cocok untuk ini.
# di file: tests/test_data_fetcher.py
import unittest
from unittest.mock import patch, Mock
from my_app.data_fetcher import get_user_data
class TestDataFetcher(unittest.TestCase):
def test_get_user_data_with_context_manager(self):
"""Menguji penggunaan patch sebagai context manager."""
with patch('my_app.data_fetcher.requests.get') as mock_get:
# Mengonfigurasi mock di dalam blok 'with'
mock_response = Mock()
mock_response.json.return_value = {'id': 2, 'name': 'Jane Doe'}
mock_get.return_value = mock_response
user_data = get_user_data(2)
mock_get.assert_called_once_with('https://api.example.com/users/2')
self.assertEqual(user_data, {'id': 2, 'name': 'Jane Doe'})
# Di luar blok 'with', requests.get kembali ke keadaan semula
Konsep Krusial: Di Mana Harus Melakukan Patch?
Ini adalah satu-satunya sumber kebingungan yang paling umum saat menggunakan `patch`. Aturannya adalah: Anda harus me-patch objek di tempat ia dicari (looked up), bukan di tempat ia didefinisikan.
Mari kita ilustrasikan dengan sebuah contoh. Misalkan kita memiliki dua file:
# di file: services.py
class Database:
def connect(self):
# ... logika koneksi yang rumit ...
return "REAL_CONNECTION"
# di file: main_app.py
from services import Database
def start_app():
db = Database()
connection = db.connect()
print(f"Got connection: {connection}")
return connection
Sekarang, kita ingin menguji `start_app` di `main_app.py` tanpa membuat objek `Database` yang nyata. Kesalahan umum adalah mencoba me-patch `services.Database`.
# di file: test_main_app.py
import unittest
from unittest.mock import patch
from main_app import start_app
class TestApp(unittest.TestCase):
# INI CARA PATCH YANG SALAH!
@patch('services.Database')
def test_start_app_incorrectly(self, mock_db):
start_app()
# Tes ini akan tetap menggunakan kelas Database yang ASLI!
# INI CARA PATCH YANG BENAR!
@patch('main_app.Database')
def test_start_app_correctly(self, mock_db_class):
# Kita me-patch 'Database' di dalam namespace 'main_app'
# Mengonfigurasi instance mock yang akan dibuat
mock_instance = mock_db_class.return_value
mock_instance.connect.return_value = "MOCKED_CONNECTION"
connection = start_app()
# Memastikan bahwa mock kita digunakan
mock_db_class.assert_called_once() # Apakah kelasnya diinstansiasi?
mock_instance.connect.assert_called_once() # Apakah metode connect dipanggil?
self.assertEqual(connection, "MOCKED_CONNECTION")
Mengapa tes pertama gagal? Karena `main_app.py` mengeksekusi `from services import Database`. Ini mengimpor kelas `Database` ke dalam namespace modul `main_app`. Ketika `start_app` berjalan, ia mencari `Database` di dalam modulnya sendiri (`main_app`). Me-patch `services.Database` mengubahnya di modul `services`, tetapi `main_app` sudah memiliki referensinya sendiri ke kelas asli. Pendekatan yang benar adalah me-patch `main_app.Database`, yang merupakan nama yang sebenarnya digunakan oleh kode yang diuji.
Teknik Mocking Tingkat Lanjut
`spec` dan `autospec`: Membuat Mock Lebih Aman
`MagicMock` standar memiliki potensi kelemahan: ia akan memungkinkan Anda untuk memanggil metode apa pun dengan argumen apa pun, bahkan jika metode tersebut tidak ada pada objek asli. Hal ini dapat menyebabkan tes yang lulus tetapi menyembunyikan masalah nyata, seperti kesalahan ketik pada nama metode atau perubahan pada API objek nyata.
# Kelas asli
class Notifier:
def send_message(self, text):
# ... mengirim pesan ...
pass
# Sebuah tes dengan kesalahan ketik
from unittest.mock import MagicMock
mock_notifier = MagicMock()
# Ups, salah ketik! Metode aslinya adalah send_message
mock_notifier.send_mesage("hello") # Tidak ada error yang dimunculkan!
mock_notifier.send_mesage.assert_called_with("hello") # Asersi ini lulus!
# Tes kita hijau, tetapi kode produksi akan gagal.
Untuk mencegah hal ini, `unittest.mock` menyediakan argumen `spec` dan `autospec`.
- `spec=SomeClass`: Ini mengonfigurasi mock untuk memiliki API yang sama dengan `SomeClass`. Jika Anda mencoba mengakses metode atau atribut yang tidak ada pada kelas asli, `AttributeError` akan dimunculkan.
- `autospec=True` (atau `autospec=SomeClass`): Ini bahkan lebih kuat. Ia bertindak seperti `spec`, tetapi juga memeriksa tanda tangan panggilan dari setiap metode yang di-mock. Jika Anda memanggil metode dengan jumlah atau nama argumen yang salah, ia akan memunculkan `TypeError`, sama seperti yang akan dilakukan oleh objek asli.
from unittest.mock import create_autospec
# Membuat mock yang memiliki antarmuka yang sama dengan kelas Notifier kita
spec_notifier = create_autospec(Notifier)
try:
# Ini akan langsung gagal karena salah ketik
spec_notifier.send_mesage("hello")
except AttributeError as e:
print(e) # Output: Mock object has no attribute 'send_mesage'
try:
# Ini akan gagal karena tanda tangannya salah (tidak ada keyword 'text')
spec_notifier.send_message("hello")
except TypeError as e:
print(e) # Output: missing a required argument: 'text'
# Ini adalah cara yang benar untuk memanggilnya
spec_notifier.send_message(text="hello") # Ini berhasil!
spec_notifier.send_message.assert_called_once_with(text="hello")
Praktik terbaik: Selalu gunakan `autospec=True` saat melakukan patching. Ini membuat tes Anda lebih kuat dan tidak rapuh. `@patch('path.to.thing', autospec=True)`.
Contoh Dunia Nyata: Menguji Layanan Pemrosesan Data
Mari kita rangkum semuanya dengan contoh yang lebih lengkap. Kita memiliki `ReportGenerator` yang bergantung pada database dan sistem file.
# di file: app/services.py
class DatabaseConnector:
def get_sales_data(self, start_date, end_date):
# Pada kenyataannya, ini akan melakukan kueri ke database
raise NotImplementedError("This should not be called in tests")
class FileSaver:
def save_report(self, path, content):
# Pada kenyataannya, ini akan menulis ke file
raise NotImplementedError("This should not be called in tests")
# di file: app/reports.py
from .services import DatabaseConnector, FileSaver
class ReportGenerator:
def __init__(self):
self.db_connector = DatabaseConnector()
self.file_saver = FileSaver()
def generate_sales_report(self, start_date, end_date, output_path):
"""Mengambil data penjualan dan menyimpan laporan yang diformat."""
raw_data = self.db_connector.get_sales_data(start_date, end_date)
if not raw_data:
report_content = "No sales data for this period."
else:
total_sales = sum(item['amount'] for item in raw_data)
report_content = f"Total Sales from {start_date} to {end_date}: ${total_sales:.2f}"
self.file_saver.save_report(path=output_path, content=report_content)
return True
Sekarang, mari kita tulis tes unit untuk `ReportGenerator.generate_sales_report` yang me-mock dependensinya.
# di file: tests/test_reports.py
import unittest
from datetime import date
from unittest.mock import patch, Mock
from app.reports import ReportGenerator
class TestReportGenerator(unittest.TestCase):
@patch('app.reports.FileSaver', autospec=True)
@patch('app.reports.DatabaseConnector', autospec=True)
def test_generate_sales_report_with_data(self, mock_db_connector_class, mock_file_saver_class):
"""Menguji pembuatan laporan ketika database mengembalikan data."""
# Arrange: Siapkan mock kita
mock_db_instance = mock_db_connector_class.return_value
mock_file_saver_instance = mock_file_saver_class.return_value
# Konfigurasikan mock database untuk mengembalikan beberapa data palsu (Stub)
fake_data = [
{'id': 1, 'amount': 100.50},
{'id': 2, 'amount': 75.00},
{'id': 3, 'amount': 25.25}
]
mock_db_instance.get_sales_data.return_value = fake_data
start = date(2023, 1, 1)
end = date(2023, 1, 31)
path = '/reports/sales_jan_2023.txt'
# Act: Buat instance dari kelas kita dan panggil metodenya
generator = ReportGenerator()
result = generator.generate_sales_report(start, end, path)
# Assert: Verifikasi interaksi dan hasilnya
# 1. Apakah database dipanggil dengan benar?
mock_db_instance.get_sales_data.assert_called_once_with(start, end)
# 2. Apakah file saver dipanggil dengan konten yang benar dan telah dihitung?
expected_content = "Total Sales from 2023-01-01 to 2023-01-31: $200.75"
mock_file_saver_instance.save_report.assert_called_once_with(
path=path,
content=expected_content
)
# 3. Apakah metode kita mengembalikan nilai yang benar?
self.assertTrue(result)
Tes ini secara sempurna mengisolasi logika di dalam `generate_sales_report` dari kompleksitas database dan sistem file, sambil tetap memverifikasi bahwa ia berinteraksi dengan mereka secara benar.
Praktik Terbaik untuk Mocking yang Efektif
- Jaga Agar Mock Tetap Sederhana: Tes yang memerlukan konfigurasi mock yang sangat kompleks sering kali merupakan pertanda (sebuah "test smell") bahwa unit yang diuji terlalu kompleks dan mungkin melanggar Prinsip Tanggung Jawab Tunggal. Pertimbangkan untuk merefaktor kode produksi.
- Mock Kolaborator, Bukan Semuanya: Anda hanya boleh me-mock objek yang berkomunikasi dengan unit yang Anda uji (kolaboratornya). Jangan me-mock objek yang sedang Anda uji itu sendiri.
- Pilih `autospec=True`: Seperti yang disebutkan, ini membuat tes Anda lebih kuat dengan memastikan antarmuka mock cocok dengan antarmuka objek asli. Ini membantu menangkap masalah yang disebabkan oleh refactoring.
- Satu Mock per Tes (Idealnya): Tes unit yang baik berfokus pada satu perilaku atau interaksi tunggal. Jika Anda mendapati diri Anda me-mock banyak objek berbeda dalam satu tes, mungkin lebih baik untuk membaginya menjadi beberapa tes yang lebih terfokus.
- Spesifik dalam Asersi Anda: Jangan hanya memeriksa `mock.method.assert_called()`. Gunakan `assert_called_with(...)` untuk memastikan interaksi terjadi dengan data yang benar. Ini membuat tes Anda lebih berharga.
- Tes Anda Adalah Dokumentasi: Gunakan nama yang jelas dan deskriptif untuk tes dan objek mock Anda (misalnya, `mock_api_client`, `test_login_fails_on_network_error`). Ini membuat tujuan tes menjadi jelas bagi pengembang lain.
Kesimpulan
Test double bukan hanya alat untuk pengujian; mereka adalah bagian mendasar dari perancangan perangkat lunak yang dapat diuji, modular, dan dapat dipelihara. Dengan mengganti dependensi nyata dengan pengganti yang terkontrol, Anda dapat membuat rangkaian pengujian yang cepat, andal, dan mampu memverifikasi setiap sudut logika aplikasi Anda.
Pustaka unittest.mock Python menyediakan perangkat kelas dunia untuk mengimplementasikan pola-pola ini. Dengan menguasai MagicMock, `patch`, dan keamanan `autospec`, Anda membuka kemampuan untuk menulis tes unit yang benar-benar terisolasi. Ini memberdayakan Anda untuk membangun aplikasi kompleks dengan percaya diri, mengetahui bahwa Anda memiliki jaring pengaman dari tes yang presisi dan bertarget untuk menangkap regresi dan memvalidasi fitur baru. Jadi, silakan mulai melakukan patching, dan bangun aplikasi Python yang lebih kuat hari ini.